package library.logger;

import android.content.Context;
import android.text.TextUtils;
import android.util.Log;

import org.json.JSONArray;
import org.json.JSONObject;

import java.io.ByteArrayOutputStream;
import java.io.PrintStream;
import java.io.StringReader;
import java.io.StringWriter;
import java.util.ArrayList;
import java.util.regex.Matcher;
import java.util.regex.Pattern;

import javax.xml.transform.OutputKeys;
import javax.xml.transform.Source;
import javax.xml.transform.Transformer;
import javax.xml.transform.TransformerFactory;
import javax.xml.transform.stream.StreamResult;
import javax.xml.transform.stream.StreamSource;


/**
 * Created by Mr.Fan on 2015/12/12.
 */
public class Logger {
    private static final int MAX_LENGTH = 4000;

    private static final String OUTER_DIVIDER = "═══════════════════════════════════";
    private static final String INNER_DIVIDER = "───────────────────────────────────";
    private static final String DIVIDER_OUTER = OUTER_DIVIDER + OUTER_DIVIDER;
    private static final String DIVIDER_INNER = INNER_DIVIDER + INNER_DIVIDER;
    private static final String METHOD_HEADER = "at ";
    private static final String METHOD_CAUSED = "Caused by: ";

    private static final String ATTHREAD = "@ Thread : ";
    private static final String ATTIME = "@ Time   : ";
    private static final String ATLOG = "@ Log    : ";
    private static final String ALIGN = "           ";

    private static String TAG;
    private static String FILTER;
    private static boolean debug;
    private static ILogger iLogger = null;

    static {
        TAG = "Logger";
        FILTER = null;
        debug = false;
    }

    private Logger() {
    }

    public static void init(Context context) {
        init(context, debug);
    }

    public static void init(String packageName) {
        init(packageName, debug);
    }

    public static void init(Context context, boolean isDebug) {
        String packageName = context.getPackageName();
        init(packageName, isDebug);
    }

    public static void init(String packageName, boolean isDebug) {
        debug = isDebug;
        if (TextUtils.isEmpty(packageName))
            return;
        setTag(packageName);
        setFilter(packageName);
    }

    public static void setLogger(ILogger interceptor) {
        Logger.iLogger = interceptor;
    }

    private static void setTag(String packageName) {
        Pattern pattern = Pattern.compile("([a-zA-Z]+\\.[a-zA-Z]+)$");
        Matcher matcher = pattern.matcher(packageName);
        if (matcher.find()) {
            TAG = matcher.group(1);
        } else
            TAG = packageName;
    }

    private static void setFilter(String packageName) {
        String[] split = packageName.split("\\.");
        FILTER = split.length > 2 ? split[0] + "." + split[1] : packageName;
//        output(false, "\n\r");
//        output(true, "*---->");
//        output(false, "Logger init done (TAG:" + TAG + ",FILTER:" + FILTER + ")");
//        output(true, "*---->");
    }

    public static void debug(String fmt, Object... args) {
        if (debug)
            try {
                print(String.format(fmt, args));
            } catch (Exception e) {
                print(fmt);
            }
    }

    public static void debug(Object obj) {
        if (debug || obj instanceof Throwable)
            print(obj);
    }

    public static void simple(String fmt, Object... args) {
        if (debug)
            try {
                output(false, String.format(fmt, args));
            } catch (Exception e) {
                output(false, fmt);
            }
    }

    public static void simple(Object obj) {
        if (obj instanceof Throwable)
            output(true, obj);
        else if (debug) {
            output(false, obj);
        }
    }

    public static void json(String json) {
        if (debug)
            try {
                if (json.startsWith("{")) {
                    JSONObject jsonObject = new JSONObject(json);
                    String message = jsonObject.toString(4);
                    debug(message);
                } else if (json.startsWith("[")) {
                    JSONArray jsonArray = new JSONArray(json);
                    String message = jsonArray.toString(4);
                    debug(message);
                }
            } catch (Exception e) {
                debug(json);
            }
    }

    public static void xml(String xml) {
        if (debug)
            try {
                Source xmlInput = new StreamSource(new StringReader(xml));
                StreamResult xmlOutput = new StreamResult(new StringWriter());
                Transformer transformer = TransformerFactory.newInstance().newTransformer();
                transformer.setOutputProperty(OutputKeys.INDENT, "yes");
                transformer.setOutputProperty("{http://xml.apache.org/xslt}indent-amount", "2");
                transformer.transform(xmlInput, xmlOutput);
                debug(xmlOutput.getWriter().toString().replaceFirst(">", ">\n"));
            } catch (Exception e) {
                debug(xml);
            }
    }

    public static void print(Object obj) {
        if (iLogger != null) {
            iLogger.onOutput(obj);
            return;
        }

        logSequence(obj);
        if (obj instanceof Throwable) {
            Throwable throwable = (Throwable) obj;
            if (throwable.getCause() != null) {
                print(throwable.getCause());
            }
        }
    }

    private static void output(boolean isError, Object log) {
        if (isError) {
            Log.e(TAG, String.valueOf(log));
        } else {
            Log.d(TAG, String.valueOf(log));
        }
    }

    public static String getDebugTrace() {
        StackTraceElement[] ste;
        ste = Thread.currentThread().getStackTrace();
        StringBuilder sb = new StringBuilder();
        for (int i = 0; i < ste.length; i++) {
            sb.append(METHOD_HEADER);
            sb.append(ste[i].getClassName());
            sb.append(".");
            sb.append(ste[i].getMethodName());
            sb.append("(");
            sb.append(ste[i].getFileName());
            sb.append(":");
            sb.append(ste[i].getLineNumber());
            sb.append(")");
            sb.append(WordUtils.CRLF);
        }
        return sb.toString();
    }

    public static String getErrorTrace(Throwable t) {
        ByteArrayOutputStream baos = new ByteArrayOutputStream();
        try {
            t.printStackTrace(new PrintStream(baos));
        } finally {
            try {
                baos.close();
            } catch (Exception e) {
            }
        }
        return baos.toString();
    }


    private static synchronized void logSequence(Object obj) {
        String msg;
        String trace;
        boolean isError = false;
        if (obj == null) {
            trace = getDebugTrace();
            msg = null;
        } else if (obj instanceof Throwable) {
            isError = true;
            Throwable t = (Throwable) obj;
            msg = obj.getClass().getSimpleName() + (t.getMessage() == null ? "" : " : " + t.getMessage());
            trace = getErrorTrace(t);
        } else {
            msg = String.valueOf(obj);
            trace = getDebugTrace();
        }

        output(isError, DIVIDER_OUTER);
        output(isError, ATTHREAD + Thread.currentThread().getName() + WordUtils.CRLF);

        logStack(isError, trace);

        output(isError, DIVIDER_INNER);
        output(isError, ATLOG);

        logFormat(isError, msg);

        output(isError, DIVIDER_OUTER);

    }

    private static void logFormat(boolean isError, String log) {
        if (TextUtils.isEmpty(log))
            return;
        boolean wrap = false;
        if (wrap)
            log = WordUtils.wrap(log, (DIVIDER_OUTER.length() - ALIGN.length()) * 2);
        String[] trace = log.split(WordUtils.CRLF);
        for (int i = 0; i < trace.length; i++) {
            output(isError, ALIGN + trace[i]);
        }

    }

    private static void logStack(boolean isError, String stack) {
        if (TextUtils.isEmpty(stack))
            return;
        final String filter = FILTER != null ? FILTER : "";
        final Pattern pattern = Pattern.compile("^(.*)\\((.*\\.*):(\\d*)\\)$");

        final String[] trace = stack.split(WordUtils.CRLF);
        final ArrayList<String> list = new ArrayList<>();
        int min = -1;
        boolean isInnerClass = false;
        for (int i = 0; i < trace.length; i++) {
            String info = trace[i];
            if (info == null)
                continue;
            if (info.startsWith(METHOD_CAUSED))
                break;
            Matcher matcher = pattern.matcher(info);
            if (matcher != null && matcher.matches()) {
                try {
                    final String clz = matcher.group(1);
                    final String file = matcher.group(2);
                    final String line = matcher.group(3);
//                    output(true, String.format("[%d/%d]:min=%d,%s", i, trace.length, min, info));
                    if (isError || file.startsWith("Logger.java")) {
                        if (min == -1)
                            min = 0;
                        if (!isError)
                            continue;
                    }
                    if (convertStrToInt(line) < 0) {
                        continue;
                    }
                    if (min == 0 && clz.contains(filter)) {
                        isInnerClass = true;
                    }
                    if (min >= 0) {
                        if (isInnerClass) {
                            if (clz.contains(filter))
                                list.add(info);
                        } else if (min < 5) {
                            list.add(info);
                            min++;
                        }
                    }
                } catch (Exception e) {

                }
            }
        }
//        output(true, list.size());
        for (int i = list.size() - 1; i >= 0; i--) {
            output(isError, list.get(i));
        }
    }

    private static int convertStrToInt(String str) throws Exception {
        int prefix = 1;
        if (str.startsWith("-")) {
            str = str.substring(1);
            prefix = -1;
        }
        return prefix * Integer.parseInt(str);
    }
}
